--[[
Copyright (C) 2008-2009 Adirelle

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
--]]

if not Yatba then return end
local Yatba = Yatba

local pairs, next, type, error, assert = pairs, next, type, error, assert
local bars, watchedGUIDs, new, del = Yatba.bars, Yatba.watchedGUIDs, Yatba.new, Yatba.del

--------------------------------------------------------------------------------
-- Database upvalue
--------------------------------------------------------------------------------

local db
Yatba.RegisterSignal('BarPrototypesDB', 'DatabaseSet', function() db = Yatba.db.profile end)

--------------------------------------------------------------------------------
-- Prototype with inheritance
--------------------------------------------------------------------------------

local NewPrototype
do
	local function Create(prototype, ...)
		if not prototype.metatable then
			assert("Cannot create an instance from "..tostring(prototype), 2)
		end
		local instance = setmetatable({}, prototype.metatable)
		instance:Initialize(...)
		return instance
	end
	
	function NewPrototype(super)
		assert(not super or super.metatable)
		local prototype = {} 
		if super then
			setmetatable(prototype, super.metatable)
			prototype.super = super
		end
		if not prototype.Create then
			prototype.Create = Create
		end
		prototype.metatable = { __index = prototype }
		return prototype
	end
end

--------------------------------------------------------------------------------
-- Base bar prototype
--------------------------------------------------------------------------------

local barPrototype = NewPrototype()

function barPrototype:Initialize(group, GUID, unitId, displayName, textTemplate)
	self.group = group
	self.unitGUID = GUID
	self.orderKey = ("%08X"):format(self:GetSerialForGUID(GUID))
	self.displayName = displayName
	self.dying = false
	self.textTemplate = textTemplate or '$n'
	self.raidIcon = unitId and GetRaidTargetIndex(unitId)

	local bar = self:CreateBar()
	self.bar = bar
	bars[bar] = self

	bar.RegisterCallback(self, 'BarReleased')
	Yatba.RegisterSignal(self, 'DropGUID_'..GUID, 'Die')
	Yatba.RegisterSignal(self, 'GroupDisabled_'..group.name, 'Remove')
	Yatba.RegisterSignal(self, 'AddonDisabled', 'Remove')
	Yatba.RegisterSignal(self, 'UpdateRaidIcon')

	Yatba.RegisterSignal(self, 'ConfigChanged_highlightTarget', 'UpdateHighlight', true)
	Yatba.RegisterSignal(self, 'ConfigChanged_showRaidIcons', 'UpdateText')
	Yatba.RegisterSignal(self, 'ConfigChanged_focusTarget', 'UpdateHighlight', true)
	Yatba.RegisterSignal(self, 'ConfigChanged_highlightTexture', 'UpdateHighlight', true)
	Yatba.RegisterSignal(self, 'ColorChanged_targetHighlight', 'UpdateHighlight', true)
	Yatba.RegisterSignal(self, 'ColorChanged_focusHighlight', 'UpdateHighlight', true)

	Yatba.RegisterEvent(self, 'PLAYER_LEAVING_WORLD', 'Remove')
	
	self:UpdateText()
	self:UpdateHighlight(true, unitId)
end

do -- function barPrototype:GetSerialForGUID(GUID)
	local n = 0
	local serials = setmetatable({}, {
		__index = function(self, key)
			n = n + 1
			self[key] = n
			return n
		end,
		__mode = 'kv',
	})
	function barPrototype:GetSerialForGUID(GUID)
		return serials[GUID]
	end
end

function barPrototype:CreateBar()
	error("barPrototype:CreateBar should be overriden")
end

function barPrototype:CreateHighlight()
	local highlight = self.bar:CreateTexture(nil, "OVERLAY")
	self.bar.highlight = highlight
	highlight:SetAllPoints(self.bar)
	highlight:SetBlendMode("ADD")
	assert(highlight, "Could not create highlight texture for "..self.bar.name)
	return highlight
end

function barPrototype:UpdateHighlight(config, unitId)
	local unitId = unitId or self:GetUnitId()
	local color
	if unitId then
		if db.highlightTarget and UnitIsUnit(unitId, 'target') then
			color = db.colors.targetHighlight
		elseif db.highlightFocus and UnitIsUnit(unitId, 'focus') then
			color = db.colors.focusHighlight
		end
	end
	local highlight = self.bar.highlight
	if color then
		if not highlight then
			highlight = self:CreateHighlight()
		end
		highlight:SetTexture("Interface\\AddOns\\Yatba\\textures\\Highlight-"..db.highlightTexture)
		highlight:SetVertexColor(unpack(color))
		highlight:Show()
	elseif highlight then
		highlight:Hide()
	end
	if config then
		if db.highlightTarget then
			Yatba.RegisterEvent(self, "PLAYER_TARGET_CHANGED", "UpdateHighlight", false)
		else
			Yatba.UnregisterEvent(self, "PLAYER_TARGET_CHANGED")
		end
		if db.highlightFocus then
			Yatba.RegisterEvent(self, "PLAYER_FOCUS_CHANGED", "UpdateHighlight", false)
		else
			Yatba.UnregisterEvent(self, "PLAYER_FOCUS_CHANGED")
		end
	end
end

local function ExpandTemplate(template, ...)
	local text = template
	for i = 1, select('#', ...), 2 do
		local var, value = select(i, ...)
		var = '$' .. var
		if value then
			value = tostring(value):trim()
			text = text:gsub('%[([^%]]*)'..var..'([^%]]*)%]', '%1'..value..'%2'):gsub(var, value)
		else
			text = text:gsub('%[[^%]]*'..var..'[^%]]*%]', ''):gsub(var, '')
		end
	end
	Yatba:Debug(("template=%q, text=%q,"):format(template, text), "args:", ...)
	return text
end

function barPrototype:ExpandTextTemplate(...)
	return ExpandTemplate(
		self.textTemplate,
		'i', (db.showRaidIcons and self.raidIcon and (("|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_%d:0|t"):format(self.raidIcon)) or false),
		...
	)
end

function barPrototype:UpdateText()
	self.bar:SetLabel(self:ExpandTextTemplate())
end

function barPrototype:SetRaidIcon(icon)
	if icon ~= self.raidIcon then
		self.raidIcon = icon
		self:UpdateText()
	end
end

function barPrototype:UpdateRaidIcon(event, GUID, icon)
	if GUID == self.unitGUID then
		self:SetRaidIcon(icon)
	elseif icon == self.raidIcon then
		self:SetRaidIcon(nil)
	end
end

function barPrototype:Die(event)
	if not self.dying then
		if event then
			--Yatba:Debug('%s killed by %q', self, event)
		end
		self.dying = true
		self.bar:Fade()
		return true
	end
end

function barPrototype:Undie(event)
	if self.dying then
		if event then
			--Yatba:Debug('%s revived by %q', self, event)
		end
		self.dying = false
		self.bar:StopFade()
		return true
	end
end

function barPrototype:Remove()
	self.bar:GetGroup():RemoveBar(self.bar)
end

function barPrototype:BarReleased()
	--Yatba:Debug('%s released', self)
	if self.bar.highlight then
		self.bar.highlight:Hide()
	end
	Yatba.UnregisterAllSignals(self)
	Yatba.UnregisterAllEvents(self)
	self.bar.UnregisterAllCallbacks(self)
	bars[self.bar] = del(self)
end

function barPrototype:GetUnitId()
	return Yatba:GetUnitIdForGUID(self.unitGUID)
end

function barPrototype:IsLesserThan(other)
	return self:GetOrderKey() < other:GetOrderKey()
end

function barPrototype:GetOrderKey()
	local GUID = self.unitGUID
	local prefix = (GUID == UnitGUID('target') and "0") or (GUID == UnitGUID('focus') and "1") or "2"
	return prefix .. self.orderKey
end

--------------------------------------------------------------------------------
-- Timer bar prototype
--------------------------------------------------------------------------------

local timerPrototype = NewPrototype(barPrototype)

function timerPrototype:Initialize(group, GUID, unitId, displayName, auraName, auraType, auraCount, duration, timeLeft, texture, debuffType, isMine)
	self.auraName = auraName
	self.auraType = auraType
	self.auraIsMine = isMine
	self.auraCount = tonumber(auraCount) or 1
	self.debuffType = (auraType == 'DEBUFF') and debuffType or nil
	self.duration = duration
	self.timeEnd = GetTime() + timeLeft
	self.texture = texture

	timerPrototype.super.Initialize(self, group, GUID, unitId, displayName, group.db.textTemplate)
	self.orderKey = ("%s%1d%06d%s"):format(self.orderKey, auraType == "BUFF" and 1 or 2, duration, auraName)

	self.bar:SetTimer(timeLeft, duration)

	Yatba.RegisterSignal(self, 'DropAura_'..GUID..'_'..auraName, 'Die')
	Yatba.RegisterSignal(self, 'FilterUpdated_'..group.name, 'CheckFilter')
	Yatba.RegisterSignal(self, 'ConfigChanged_defaultFiltering', 'CheckFilter')
	self.bar.RegisterCallback(self, 'TimerFinished', 'Die')
	self.bar.RegisterCallback(self, 'TimerStopped', 'Die')

	Yatba.RegisterSignal(self, 'TextTemplateUpdated')
	
	Yatba.RegisterSignal(self, 'ColorChanged_buff', 'UpdateColor')
	Yatba.RegisterSignal(self, 'ColorChanged_debuff', 'UpdateColor')
	Yatba:Debug("debuffType: %q", self.debuffType)
	if self.debuffType then
		Yatba.RegisterSignal(self, 'ConfigChanged_debuffColorByType', 'UpdateColor')
		Yatba.RegisterSignal(self, 'ColorChanged_debuff'..self.debuffType, 'UpdateColor')
	end
	self:UpdateColor()

	self:WatchGUID()
end

function timerPrototype:CreateBar()
	return self.group:NewTimerBar(self.unitGUID..self.auraName, "X", self.duration, self.duration, self.texture, 0)
end

function timerPrototype:ExpandTextTemplate(...)
	return timerPrototype.super.ExpandTextTemplate(self,
		'n', (not self.group.db.displayHeaders and self.displayName or false),	
		's', self.auraName or false,
		'a', (self.auraCount > 1 and self.auraCount),
		...
	)
end

function timerPrototype:UpdateColor()
	local bar = self.bar
	local colorName = self.auraType:lower() .. (db.debuffColorByType and self.debuffType or "")
	Yatba:Debug("colorName: %q", colorName)
	bar:UnsetAllColors()
	bar:SetColorAt(0, unpack(db.colors[colorName]))
end

function timerPrototype:CheckFilter(unitId)
	unitId = unitId or self:GetUnitId()
	if unitId and not self.group:AcceptTimer(unitId, self.auraName, self.auraType, self.duration, self.auraIsMine) then
		self:Die()
		return false
	else
		return true
	end
end

function timerPrototype:Refresh(timeLeft, duration, count, unitId)
	if timeLeft > 0 then
		self.duration = duration
		self.timeEnd = GetTime() + timeLeft
		if self:CheckFilter(unitId) then
			self.bar:SetTimer(timeLeft, duration)
			count = tonumber(count) or 1
			if count ~= self.auraCount then
				self.auraCount = count
				self:UpdateText()
			end
			self:Undie()
			return true
		end
	else
		self:Die()
	end
end

function timerPrototype:TextTemplateUpdated(event, group, textTemplate)
	if group == self.group and textTemplate ~= self.textTemplate then
		self.textTemplate = textTemplate
		self:UpdateText()
	end
end

function timerPrototype:Die(event)
	if timerPrototype.super.Die(self, event) then
		self:UnwatchGUID()
		return true
	end
end

function timerPrototype:Undie(event)
	if timerPrototype.super.Undie(self, event) then
		self:WatchGUID()
		return true
	end
end

function timerPrototype:BarReleased(event)
	self:UnwatchGUID()
	timerPrototype.super.BarReleased(self, event)
end

function timerPrototype:WatchGUID()
	local GUID, auraName = self.unitGUID, self.auraName
	if not watchedGUIDs[GUID] then
		watchedGUIDs[GUID] = new()
	end
	if not watchedGUIDs[GUID][auraName] then
		watchedGUIDs[GUID][auraName] = self
		Yatba:SendSignal('TimerAdded_'..self.group.name, self)
	end
end

function timerPrototype:UnwatchGUID()
	local GUID, auraName = self.unitGUID, self.auraName
	if watchedGUIDs[GUID] and watchedGUIDs[GUID][auraName] then
		watchedGUIDs[GUID][auraName] = nil
		Yatba:SendSignal('TimerRemoved_'..self.group.name..'_'..GUID, self)
		if not next(watchedGUIDs[GUID]) then
			watchedGUIDs[GUID] = del(watchedGUIDs[GUID])
			Yatba:SendSignal('DropGUID_'..GUID)
		end
	end
end

--------------------------------------------------------------------------------
-- Header prototype
--------------------------------------------------------------------------------

local headerPrototype = NewPrototype(barPrototype)

function headerPrototype:Initialize(group, GUID, unitId, displayName)
	Yatba:Debug('CreateHeader', GUID, unitId, displayName)
	headerPrototype.super.Initialize(self, group, GUID, unitId, displayName, '[$i ]$n')
	Yatba.RegisterSignal(self, 'HeadersDisabled_'..group.name, 'Die')
	Yatba.RegisterSignal(self, 'TimerRemoved_'..group.name..'_'..GUID, 'RemoveTimer')
	self.timers = new()
	if watchedGUIDs[GUID] then
		for auraName, timer in pairs(watchedGUIDs[GUID]) do
			self:AddTimer(timer)
		end
	end
	Yatba.RegisterSignal(self, 'ConfigChanged_headerHealth', 'HealthUpdated', true)
	Yatba.RegisterSignal(self, 'ConfigChanged_healthGradient', 'UpdateColor', true)
	self:HealthUpdated(true, nil, unitId)
	if unitId then
		SetPortraitTexture(self.bar.icon, unitId)
		self.bar.icon:Show()
	end
end

function headerPrototype:ExpandTextTemplate(...)
	return headerPrototype.super.ExpandTextTemplate(self, 'n', self.displayName)
end

function headerPrototype:CreateBar()
	return self.group:NewCounterBar(self.unitGUID, self.displayName, 100, 100)
end

function headerPrototype:AddTimer(timer)
	if not self.timers[timer.auraName] then
		self:Undie('AddTimer')
		self.timers[timer.auraName] = true
	end
end

function headerPrototype:RemoveTimer(event, timer)
	local auraName = timer.auraName
	if self.timers[auraName] then
		self.timers[auraName] = nil
		if not next(self.timers) then
			self:Die(event)
		end
	end
end

function headerPrototype:BarReleased()
	self.timers = del(self.timers)
	headerPrototype.super.BarReleased(self)
end

function headerPrototype:HealthUpdated(config, event, unitId)
	if config then
		local signalName = 'HealthUpdated_'..self.unitGUID
		if db.headerHealth then
			Yatba.RegisterSignal(self, signalName, 'HealthUpdated', false)
		else
			Yatba.UnregisterSignal(self, signalName)
		end
		self:UpdateColor(true)
	end
	local pct
	unitId = db.headerHealth and (unitId or self:GetUnitId())
	if unitId then
		pct = 100 * UnitHealth(unitId) / UnitHealthMax(unitId)
	end
	if pct then
		self.bar:SetValue(pct)
		self.bar:SetTimerLabel(("%.0f%%"):format(pct))
		self.bar:ShowTimerLabel()
	else
		self.bar:HideTimerLabel()
	end
end

function headerPrototype:UpdateColor(config)
	local bar = self.bar
	bar:UnsetAllColors()
	if db.headerHealth and db.healthGradient then
		bar:SetColorAt(0.0,unpack(db.colors.lowHealth))
		bar:SetColorAt(1.0,unpack(db.colors.highHealth))
		if config then
			Yatba.RegisterSignal(self, 'ColorChanged_lowHealth', 'UpdateColor', false)
			Yatba.RegisterSignal(self, 'ColorChanged_highHealth', 'UpdateColor', false)
			Yatba.UnregisterSignal(self, 'ColorChanged_header')
		end
	else
		bar:SetColorAt(0,unpack(db.colors.header))
		if config then
			Yatba.UnregisterSignal(self, 'ColorChanged_lowHealth')
			Yatba.UnregisterSignal(self, 'ColorChanged_highHealth')
			Yatba.RegisterSignal(self, 'ColorChanged_header', 'UpdateColor', false)
		end
	end
end

function headerPrototype:GetOrderKey()
	return headerPrototype.super.GetOrderKey(self) .. (self.group:HasReverseGrowth() and "9" or "0")
end

--------------------------------------------------------------------------------
-- Expose public prototypes
--------------------------------------------------------------------------------

Yatba.barPrototypes = {
	timer = timerPrototype,
	header = headerPrototype,
}
